Skip to content

fix(governance): don't open the run span when the host already has one - #169

Merged
JoshParkSJ merged 3 commits into
mainfrom
fix/governance-skip-span-when-host-has-one
Sep 10, 2026
Merged

fix(governance): don't open the run span when the host already has one#169
JoshParkSJ merged 3 commits into
mainfrom
fix/governance-skip-span-when-host-has-one

Conversation

@JoshParkSJ

Copy link
Copy Markdown
Contributor

Alternative to #168 — same bug, other fix. Pick one, close the other.

Problem

Conversational agent run spans stopped showing up under the CAS Exchange span — on prod rings 1-5 and staging. (Slack thread)

In the sample production trace, the agent run span points at a parent id that isn't in the trace at all:

span id parent
Exchange 380056a28af21b0d Conversation
Conversational agent run 52f13179f466cf8f 8eb59fa2b10766c8 — not present

So it's a dropped parent, not a mis-parented child.

Cause

_governance_root_span opens a uipath.governance.run span around every execute / stream, which sits between the host's span and the agent's own run span.

Hosts filter spans on export. The Agents LLMOps exporter keeps only spans carrying uipath.custom_instrumentation and drops the rest without re-parenting their children. This span doesn't carry the marker, so it never reaches the backend — while the agent run span still references it. The agent run span and its whole subtree detach from the host's trace.

Same reason the BEFORE_AGENT guardrail rule spans (guardrail-harmful-content, -prompt-injection, -pii) surface at the trace root: they fire before the agent run span exists, so their parent was the dropped span too. The rule spans that fire during the model hooks are parented on the agent run span and look correct.

Note this is not the Governance evaluation span from LLM Gateway — that one is correctly nested under LLM call. Two similarly-named spans; the thread chased the wrong one for a bit.

Fix

Skip the span when a valid span context is already current.

Under a host, the span's only contribution was a shared trace_id — which the host's span already supplies. Governance events still correlate exactly as before, and the agent run span attaches directly to Exchange. With no ambient span (standalone / CLI runs) the wrapper still opens a root span, unchanged.

Customer-facing traces look the way they did before the span was introduced.

Versus #168

this PR #168
agent run reattaches to Exchange yes yes, through the governance span
orphaned guardrail spans fixed yes yes
uipath.governance.run in customer traces never new level between Exchange and the agent run
span's agent_name / runtime_id under a host lost visible

Comes down to whether governance wants that span in customer-facing traces.

Follow-up worth doing separately (either way)

The export filter in uipath-agents-python should re-parent survivors to their nearest surviving ancestor. Any future filtered span will orphan its children exactly this way.

Tests

Three added to tests/test_governance_runtime.py:

  • under a host span nothing is exported and the delegate's span parents directly on the host span
  • with no host span the root span is still opened and keeps its agent_name / runtime_id
  • stream takes the same path as execute

Full suite passes (435).

🤖 Generated with Claude Code

The governance runtime opens a `uipath.governance.run` span around
every agent invocation, between the host's span and the agent's own
run span. Hosts filter spans on export — the Agents LLMOps exporter
keeps only spans carrying `uipath.custom_instrumentation` and drops
the rest without re-parenting their children. This span doesn't carry
the marker, so it is dropped while the agent run span still references
it, leaving the agent run span pointing at a parent id that never
reaches the backend.

Observed in production: conversational agent run spans stopped
appearing under the CAS Exchange span across prod rings 1-5 and
staging, and the BEFORE_AGENT guardrail rule spans surfaced at the
trace root for the same reason.

Skip the span when a valid span context is already current. The host's
span supplies the trace_id that was this span's only purpose there, so
governance events still correlate and the agent run span attaches
directly to the host's span. With no ambient span the wrapper still
opens a root span, so standalone runs keep one trace per agent run.

Alternative to #168, which fixes the same break by marking the span
for export instead. That keeps the span but surfaces it in
customer-facing traces as a new level between Exchange and the agent
run; this one keeps traces unchanged but gives up the span.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new tests rely on patching a private OpenTelemetry global in a way that may not reliably restore state across OpenTelemetry implementations/versions, making the suite brittle.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adjusts the governance runtime’s OpenTelemetry wrapping so it does not open the uipath.governance.run span when an ambient valid span context already exists, preventing downstream spans from being orphaned when hosts drop the wrapper span during export.

Changes:

  • Make _governance_root_span() a no-op when a valid current span context is already active.
  • Add tests that validate execute/stream behavior under a host span vs. no host span, including exported-span assertions.
File summaries
File Description
src/uipath/runtime/governance/runtime.py Skip opening uipath.governance.run when a valid ambient span context exists, to avoid creating a dropped parent span.
tests/test_governance_runtime.py Add in-memory-exporter tests covering host-span vs. no-host-span scenarios for both execute and stream.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/uipath/runtime/governance/runtime.py Outdated
Comment thread tests/test_governance_runtime.py Outdated
Comment thread src/uipath/runtime/governance/runtime.py Outdated
- Drop the None check on get_current_span — OTel returns INVALID_SPAN,
  never None, so the span-context validity check is sufficient.
- Reword the behavior-matrix bullet: the no-op triggers on any valid
  current span context, including a remotely propagated one, not only
  a host-opened span.
- Snapshot and restore the same private tracer-provider global in the
  test helper rather than mixing it with get_tracer_provider().
- Assert the agent span's parent off the exported ReadableSpan; the
  live Span protocol has no parent attribute, which failed mypy in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@JoshParkSJ
JoshParkSJ merged commit 9d984e8 into main Sep 10, 2026
55 of 62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants